#include "esp_camera.h"
#include <WiFi.h>
#include <esp_http_server.h>

const char* ssid = "свой роутер";
const char* password = "свой пароль";

#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

// Инновационный сверхлёгкий HTML-интерфейс с ручными ползунками
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ДОЗОР MAX КОНТРОЛЬ</title>
    <style>
        body { font-family: sans-serif; background: #1a1a1a; color: #fff; text-align: center; margin: 0; padding: 10px; }
        .container { max-width: 500px; margin: 0 auto; background: #2a2a2a; padding: 15px; border-radius: 8px; }
        img { width: 100%; max-width: 400px; background: #000; border-radius: 5px; }
        .controls { margin-top: 15px; background: #333; padding: 12px; border-radius: 5px; text-align: left; }
        label { display: block; margin: 10px 0 4px; font-size: 14px; font-weight: bold; }
        input[type="range"] { width: 100%; height: 10px; }
        .status { font-weight: bold; color: #44ff44; margin: 15px 0; font-size: 16px; }
        span { float: right; color: #ffcc00; }
        canvas { display: none; }
    </style>
</head>
<body>
<div class="container">
    <h2>КОШАЧИЙ ДОЗОР [РУЧНОЙ МАКС]</h2>
    <img id="stream" src="/stream" crossorigin="anonymous">
    <div class="status" id="statusLine">Патрулирование...</div>
    
    <div class="controls">
        <label>Грубость пикселя: <span id="v1">15</span></label>
        <input type="range" id="motionThreshold" min="5" max="60" value="15">
        
        <label>Порог шума (размер объекта): <span id="v2">5</span>%</label>
        <input type="range" id="detectThreshold" min="1" max="30" value="5" step="0.5">
        
        <label style="margin-top:15px; color:#ffcc00; display:block; text-align:center;">
            <input type="checkbox" id="enableSave" checked> Разрешить очередь на 5 кадров
        </label>
    </div>
</div>
<canvas id="c" width="160" height="120"></canvas>
<canvas id="p" width="160" height="120"></canvas>
<script>
    const img = document.getElementById('stream');
    const c = document.getElementById('c').getContext('2d');
    const p = document.getElementById('p').getContext('2d');
    const status = document.getElementById('statusLine');
    const s1 = document.getElementById('motionThreshold');
    const s2 = document.getElementById('detectThreshold');
    const v1 = document.getElementById('v1');
    const v2 = document.getElementById('v2');
    const enableSave = document.getElementById('enableSave');

    s1.oninput = () => v1.innerText = s1.value;
    s2.oninput = () => v2.innerText = s2.value;

    let first = true, lastSave = 0;

    setInterval(() => {
        if (!img.complete || img.naturalWidth === 0) return;
        try {
            c.drawImage(img, 0, 0, 160, 120);
            if (first) { p.drawImage(img, 0, 0, 160, 120); first = false; return; }
            const cur = c.getImageData(0, 0, 160, 120).data;
            const prv = p.getImageData(0, 0, 160, 120).data;
            let diff = 0;
            const th = parseInt(s1.value);

            for (let i = 0; i < cur.length; i += 16) {
                if (Math.abs(cur[i] - prv[i]) > th) diff++;
            }
            const pct = (diff / (160 * 120 / 4)) * 100;
            const limit = parseFloat(s2.value);

            if (pct > limit && (Date.now() - lastSave > 7000)) {
                lastSave = Date.now();
                status.innerText = `ТРЕВОГА! Движение: ${pct.toFixed(1)}%`;
                status.style.color = '#ff4444';
                
                if (enableSave.checked) {
                    let shot = 0;
                    function fire() {
                        if (shot >= 5) { status.innerText = "Очередь снята. Наблюдение..."; status.style.color = '#44ff44'; return; }
                        c.drawImage(img, 0, 0, 160, 120);
                        const a = document.createElement('a');
                        a.download = `CATCH_${shot+1}_${Date.now()}.jpg`;
                        a.href = document.getElementById('c').toDataURL('image/jpeg', 0.75);
                        a.click();
                        shot++;
                        setTimeout(fire, 150); // Интервал между кадрами 150мс
                    }
                    fire();
                }
            } else if (Date.now() - lastSave > 2000) {
                status.innerText = `Анализ: Чисто (${pct.toFixed(1)}%)`;
                status.style.color = '#44ff44';
            }
            p.drawImage(document.getElementById('c'), 0, 0);
        } catch (e) {}
    }, 60);
</script>
</body>
</html>
)rawliteral";

void startCameraServer();

void setup() {
  Serial.begin(115200);
  
  // Глушим Brownout жесткой записью в адрес регистра напрямую
  volatile uint32_t* rtc_reg = (volatile uint32_t*)(0x3ff48000 + 0xdc);
  *rtc_reg = 0;

  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  
  // УЛЬТРА-СКОРОСТНЫЕ НАСТРОЙКИ БЕЗ ЛАГОВ И ЗАДЕРЖЕК
  config.xclk_freq_hz = 20000000;         // Возвращаем шину на 20 МГц
  config.pixel_format = PIXFORMAT_GRAYSCALE; // ПЕРЕВЕДЕНО В ЧЕРНО-БЕЛЫЙ РЕЖИМ (Кадры пролетают мгновенно)
  config.frame_size = FRAMESIZE_QVGA;       // Размер 320x240
  config.jpeg_quality = 18; // Снижаем качество сжатия (уменьшаем вес файла по вафле)
  config.fb_count = 2; // Режим двойного буфера активен

  if (esp_camera_init(&config) != ESP_OK) { while(true) { delay(1000); } }

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); }
  
  startCameraServer();
  Serial.print("\nURL: http://");
  Serial.println(WiFi.localIP());
}

void loop() { delay(10000); }

void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;
  config.ctrl_port = 32768;
  
  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = [](httpd_req_t *req) -> esp_err_t {
      httpd_resp_set_type(req, "text/html");
      return httpd_resp_send(req, index_html, -1); 
    },
    .user_ctx  = NULL
  };

  httpd_uri_t stream_uri = {
    .uri       = "/stream",
    .method    = HTTP_GET,
    .handler   = [](httpd_req_t *req) -> esp_err_t {
      camera_fb_t * fb = NULL;
      esp_err_t res = ESP_OK;
      size_t _jpg_buf_len = 0;
      uint8_t * _jpg_buf = NULL;
      
      // Локальный буфер заменен на статический безопасный массив внутри обработчика потока
      static char static_part_buf[64];

      res = httpd_resp_set_type(req, "multipart/x-mixed-replace;boundary=123456789000000000000987654321");
      if(res != ESP_OK) return res;

      while(true){
        fb = esp_camera_fb_get();
        if (!fb) { res = ESP_FAIL; } 
        else {
          bool jpeg_converted = fmt2jpg(fb->buf, fb->len, fb->width, fb->height, fb->format, 70, &_jpg_buf, &_jpg_buf_len);
          if(!jpeg_converted) res = ESP_FAIL;
        }
        if(res == ESP_OK){
          size_t hlen = snprintf(static_part_buf, 64, "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n", _jpg_buf_len);
          res = httpd_resp_send_chunk(req, static_part_buf, hlen);
        }
        if(res == ESP_OK) res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
        if(res == ESP_OK) res = httpd_resp_send_chunk(req, "\r\n--123456789000000000000987654321\r\n", 36);
        if(_jpg_buf) { free(_jpg_buf); _jpg_buf = NULL; }
        if(fb) { esp_camera_fb_return(fb); }
        if(res != ESP_OK) break;
      }
      return res;
    },
    .user_ctx  = NULL
  };
  
  httpd_handle_t camera_httpd = NULL;
  httpd_start(&camera_httpd, &config);
  httpd_register_uri_handler(camera_httpd, &index_uri);
  httpd_register_uri_handler(camera_httpd, &stream_uri);
}
